﻿package net.chrisjrock.loader.bulletloader
{
	import flash.display.Loader;
	import flash.display.LoaderInfo;
	import flash.events.AsyncErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.media.Sound;
	import flash.media.SoundLoaderContext;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.system.ApplicationDomain;
	import flash.system.LoaderContext;
	import flash.utils.Dictionary;
	
	/**
	 * ...
	 * @author Christopher J. Rock
	 */
	public class BulletLoader extends EventDispatcher
	{
		////////////////////////////////////////////////////////////////////Static
		public static const ALL_LOADS_COMPLETE:String = "allLoadsComplete";
		
		public static var eventDispatcher:EventDispatcher = new EventDispatcher();
		
		private static var _loadsMax:uint = 0;
		private static var _loadsRunning:uint;
		private static var _loadQueue:Array = [];
		private static var _loadArguments:Dictionary = new Dictionary();
		
		public static const COMPLETE:String = Event.COMPLETE;
		public static const PROGRESS:String = ProgressEvent.PROGRESS;
		public static const OPEN:String = Event.OPEN;
		//public static const INIT:String = Event.INIT;
		public static const IO_ERROR:String = IOErrorEvent.IO_ERROR;
		public static const ASYNC_ERROR:String = AsyncErrorEvent.ASYNC_ERROR;
		public static const SECURITY_ERROR:String = SecurityErrorEvent.SECURITY_ERROR;
		
		public static const ITEM_COMPLETE:String = "itemComplete";
		public static const ITEM_OPEN:String = "itemOpen";
		public static const ITEM_IO_ERROR:String = "itemIoError";
		public static const ITEM_ASYNC_ERROR:String = "itemIoError";
		public static const ITEM_SECURITY_ERROR:String = "itemIoError";
		
		private static var _dataFormat:String = URLLoaderDataFormat.TEXT;
		private static var _urlRequestMethod:String = URLRequestMethod.GET;
		private static var _displayContext:LoaderContext;
		private static var _soundContext:SoundLoaderContext;
		
		public static var dispatchStaticErrors:Boolean = true;
		
		public static function loadData (url:String , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):URLLoader
		{
			if (!url)
			{
				throw (new Error("BulletLoader: loadData: url argument must be non-null."));
			}
			
			if (!url.length)
			{
				throw (new Error("BulletLoader: loadData: url argument must have length greater than 0."));
			}
			
			var loader:URLLoader = new URLLoader();
			addLoaderListeners(loader , completeHandler , ioErrorHandler , progressHandler , openHandler , asyncErrorHandler , securityErrorHandler);
			loader.dataFormat = _dataFormat;
			
			if (!_loadsMax || _loadsRunning < _loadsMax)
			{
				_loadsRunning++;
				loader.addEventListener(COMPLETE , completeCheckGlobal , false , int.MAX_VALUE);
				
				var internalErrorHandler:Function;
				if (Boolean(ioErrorHandler))
				{
					internalErrorHandler = errorCheckGlobal;
				}
				else
				{
					internalErrorHandler = errorRelayGlobal;
				}
				
				loader.addEventListener(IO_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				loader.addEventListener(ASYNC_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				loader.addEventListener(SECURITY_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				
				var request:URLRequest = new URLRequest(BulletPath.applyPath(url));
				request.method = _urlRequestMethod;
				loader.load(request);
			}
			else
			{
				_loadQueue.push(loader);
				_loadArguments[loader] = new Array(new URLRequest(BulletPath.applyPath(url)));
			}
			
			return loader;
		}
		
		public static function loadSound (url:String , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):Sound
		{
			if (!url)
			{
				throw (new Error("BulletLoader: loadSound: url argument must be non-null."));
			}
			
			if (!url.length)
			{
				throw (new Error("BulletLoader: loadSound: url argument must have length greater than 0."));
			}
			
			var loader:Sound = new Sound();
			addLoaderListeners(loader , completeHandler , ioErrorHandler , progressHandler , openHandler , asyncErrorHandler , securityErrorHandler);
			
			if (!_loadsMax || _loadsRunning < _loadsMax)
			{
				_loadsRunning++;
				loader.addEventListener(COMPLETE , completeCheckGlobal , false , int.MAX_VALUE);
				
				var internalErrorHandler:Function;
				if (Boolean(ioErrorHandler))
				{
					internalErrorHandler = errorCheckGlobal;
				}
				else
				{
					internalErrorHandler = errorRelayGlobal;
				}
				
				loader.addEventListener(IO_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				loader.addEventListener(ASYNC_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				loader.addEventListener(SECURITY_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				
				loader.load(new URLRequest(BulletPath.applyPath(url)) , _soundContext);
			}
			else
			{
				_loadQueue.push(loader);
				_loadArguments[loader] = new Array(new URLRequest(BulletPath.applyPath(url)) , _soundContext);
			}
			
			return loader;
		}
		
		public static function loadDisplay (url:String , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):Loader
		{
			if (!url)
			{
				throw (new Error("BulletLoader: loadDisplay: url argument must be non-null."));
			}
			
			if (!url.length)
			{
				throw (new Error("BulletLoader: loadDisplay: url argument must have length greater than 0."));
			}
			
			var loader:Loader = new Loader();
			addLoaderListeners(loader.contentLoaderInfo , completeHandler , ioErrorHandler , progressHandler , openHandler , asyncErrorHandler , securityErrorHandler);
			
			var displayContext:LoaderContext;
			if (_displayContext)
			{
				displayContext = _displayContext;
			}
			else
			{
				displayContext = new LoaderContext(true , ApplicationDomain.currentDomain);
			}
			
			if (!_loadsMax || _loadsRunning < _loadsMax)
			{
				_loadsRunning++;
				loader.contentLoaderInfo.addEventListener(COMPLETE , completeCheckGlobal , false , int.MAX_VALUE);
				
				var internalErrorHandler:Function;
				if (Boolean(ioErrorHandler))
				{
					internalErrorHandler = errorCheckGlobal;
				}
				else
				{
					internalErrorHandler = errorRelayGlobal;
				}
				
				loader.contentLoaderInfo.addEventListener(IO_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				loader.contentLoaderInfo.addEventListener(ASYNC_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				loader.contentLoaderInfo.addEventListener(SECURITY_ERROR , internalErrorHandler , false , int.MAX_VALUE);
				
				loader.load(new URLRequest(BulletPath.applyPath(url)) , displayContext);
			}
			else
			{
				_loadQueue.push(loader);
				_loadArguments[loader] = new Array(new URLRequest(BulletPath.applyPath(url)) , displayContext);
			}
			
			return loader;
		}
		
		private static function startNextLoad ():void
		{
			if (_loadQueue.length > 0)
			{
				var nextLoad:* = _loadQueue.shift();
				var loaderEventDispatcher:IEventDispatcher;
				if (nextLoad is URLLoader || nextLoad is Sound)
				{
					loaderEventDispatcher = nextLoad;
				}
				else
				{
					loaderEventDispatcher = nextLoad.contentLoaderInfo;
				}
				
				loaderEventDispatcher.addEventListener(COMPLETE , completeCheckGlobal , false , int.MAX_VALUE);
				loaderEventDispatcher.addEventListener(IO_ERROR , errorCheckGlobal , false , int.MAX_VALUE);
				loaderEventDispatcher.addEventListener(ASYNC_ERROR , errorCheckGlobal , false , int.MAX_VALUE);
				loaderEventDispatcher.addEventListener(SECURITY_ERROR , errorCheckGlobal , false , int.MAX_VALUE);
				
				nextLoad.load.apply(nextLoad , _loadArguments[nextLoad]);
			}
			else if (_loadsRunning > 0)
			{
				_loadsRunning--;
				if (_loadsRunning == 0)
				{
					eventDispatcher.dispatchEvent(new Event(ALL_LOADS_COMPLETE));
				}
			}
		}
		
		private static function completeCheckGlobal (event:Event):void
		{
			event.target.removeEventListener(COMPLETE , completeCheckGlobal);
			event.target.removeEventListener(IO_ERROR , errorCheckGlobal);
			event.target.removeEventListener(ASYNC_ERROR , errorCheckGlobal);
			event.target.removeEventListener(SECURITY_ERROR , errorCheckGlobal);
			
			startNextLoad();
		}
		
		private static function errorCheckGlobal (event:Event):void
		{
			event.target.removeEventListener(COMPLETE , completeCheckGlobal);
			event.target.removeEventListener(IO_ERROR , errorCheckGlobal);
			event.target.removeEventListener(ASYNC_ERROR , errorCheckGlobal);
			event.target.removeEventListener(SECURITY_ERROR , errorCheckGlobal);
			
			startNextLoad();
		}
		
		//This is used for io, async, and security errors.
		private static function errorRelayGlobal (event:*):void
		{
			event.target.removeEventListener(COMPLETE , completeCheckGlobal);
			event.target.removeEventListener(IO_ERROR , errorRelayGlobal);
			event.target.removeEventListener(ASYNC_ERROR , errorRelayGlobal);
			event.target.removeEventListener(SECURITY_ERROR , errorRelayGlobal);
			
			event.text = "BulletLoader Error: text: '" + event.text + "'";
			
			errorCheckGlobal(event);
			if (dispatchStaticErrors)
			{
				eventDispatcher.dispatchEvent(event);
			}
		}
		
		public static function addLoaderListeners (loader:IEventDispatcher , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):void
		{
			if (Boolean(completeHandler))
			{
				loader.addEventListener(COMPLETE , completeHandler);
			}
			
			if (Boolean(progressHandler))
			{
				loader.addEventListener(PROGRESS , progressHandler);
			}
			
			if (Boolean(openHandler))
			{
				loader.addEventListener(OPEN , openHandler);
			}
			
			if (Boolean(ioErrorHandler))
			{
				loader.addEventListener(IO_ERROR , ioErrorHandler);
			}
			
			if (Boolean(asyncErrorHandler))
			{
				loader.addEventListener(ASYNC_ERROR , asyncErrorHandler);
			}
			
			if (Boolean(securityErrorHandler))
			{
				loader.addEventListener(SECURITY_ERROR , securityErrorHandler);
			}
		}
		
		public static function removeLoaderListeners (loader:IEventDispatcher , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):void
		{
			if (Boolean(completeHandler))
			{
				loader.removeEventListener(COMPLETE , completeHandler);
			}
			
			if (Boolean(progressHandler))
			{
				loader.removeEventListener(PROGRESS , progressHandler);
			}
			
			if (Boolean(openHandler))
			{
				loader.removeEventListener(OPEN , openHandler);
			}
			
			if (Boolean(ioErrorHandler))
			{
				loader.removeEventListener(IO_ERROR , ioErrorHandler);
			}
			
			if (Boolean(asyncErrorHandler))
			{
				loader.removeEventListener(ASYNC_ERROR , asyncErrorHandler);
			}
			
			if (Boolean(securityErrorHandler))
			{
				loader.removeEventListener(SECURITY_ERROR , securityErrorHandler);
			}
		}
		
		public static function get loadsMax ():uint
		{
			return _loadsMax;
		}
		
		public static function set loadsMax (value:uint):void
		{
			_loadsMax = value;
		}
		
		public static function get loadsRunning ():uint
		{
			return _loadsRunning;
		}
		
		public static function get loadsQueued ():uint
		{
			return _loadQueue.length;
		}
		
		public static function get dataFormat ():String
		{
			return _dataFormat;
		}
		
		public static function set dataFormat (value:String):void
		{
			_dataFormat = value;
		}
		
		public static function get urlRequestMethod ():String
		{
			return _urlRequestMethod;
		}
		
		public static function set urlRequestMethod (value:String):void
		{
			_urlRequestMethod = value;
		}
		
		public static function get soundContext ():SoundLoaderContext
		{
			return _soundContext;
		}
		
		public static function set soundContext (value:SoundLoaderContext):void
		{
			_soundContext = value;
		}
		
		public static function get displayContext ():LoaderContext
		{
			return _displayContext;
		}
		
		public static function set displayContext (value:LoaderContext):void
		{
			_displayContext = value;
		}
		
		
		
		
		
		
		
		////////////////////////////////////////////////////////////////////Instance
		private var _loaderByKey:Dictionary = new Dictionary();
		private var _urlByLoader:Dictionary = new Dictionary();
		
		private var _keyByLoaderInfo:Dictionary = new Dictionary();
		private var _itemByKey:Dictionary = new Dictionary();
		
		private var _loading:Boolean = false;
		private var _opened:Boolean = false;
		
		private var _bytesTotalIndex:Dictionary = new Dictionary();
		private var _bytesLoadedIndex:Dictionary = new Dictionary();
		private var _bytesTotal:uint = 0;
		private var _bytesLoaded:uint = 0;
		private var _byteRatio:Number = NaN;
		
		private var _itemsTotal:uint = 0;
		private var _itemsOpened:uint = 0;
		private var _itemsLoaded:uint = 0;
		private var _ioErrors:uint = 0;
		private var _itemRatio:Number = NaN;
		
		public var dataFormat:String = BulletLoader.dataFormat;
		public var soundContext:SoundLoaderContext = BulletLoader.soundContext;
		public var displayContext:LoaderContext = BulletLoader.displayContext;
		public var dispatchErrors:Boolean = true;
		
		public var openAllBeforeProgress:Boolean = true;
		
		public function BulletLoader ():void
		{
		}
		
		public function dispose ():void
		{
			close();
			_loaderByKey = _urlByLoader = _keyByLoaderInfo = _itemByKey = _bytesTotalIndex = _bytesLoadedIndex = null;
			dataFormat = null;
			soundContext = null;
			displayContext = null;
		}
		
		public function addData(url:String , key:* = null , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):void
		{
			var loader:URLLoader = new URLLoader();
			loader.dataFormat = this.dataFormat;
			addLoaderListeners(loader , completeCheck , errorCheck , progressCheck , openCheck , errorCheck , errorCheck);
			addLoaderListeners(loader , completeHandler , ioErrorHandler , progressHandler , openHandler , asyncErrorHandler , securityErrorHandler);
			
			if (!key)
			{
				key = url;
			}
			
			_loaderByKey[key] = loader;
			_urlByLoader[loader] = url;
			_keyByLoaderInfo[loader] = key;
			_itemsTotal++;
		}
		
		public function addSound(url:String , key:* = null , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):void
		{
			var loader:Sound = new Sound();
			addLoaderListeners(loader , completeCheck , errorCheck , progressCheck , openCheck , errorCheck , errorCheck);
			addLoaderListeners(loader , completeHandler , ioErrorHandler , progressHandler , openHandler , asyncErrorHandler , securityErrorHandler);
			
			if (!key)
			{
				key = url;
			}
			
			_loaderByKey[key] = loader;
			_urlByLoader[loader] = url;
			_keyByLoaderInfo[loader] = key;
			_itemsTotal++;
		}
		
		public function addDisplay (url:String , key:* = null , completeHandler:Function = null , ioErrorHandler:Function = null , progressHandler:Function = null , openHandler:Function = null , asyncErrorHandler:Function = null , securityErrorHandler:Function = null):void
		{
			if (!url || !url.length)
			{
				throw (new Error("Quickloader: addDisplay: url argument must be non-null and have length >= 0. Current url value:" , url));
			}
			
			var loader:Loader = new Loader();
			addLoaderListeners(loader.contentLoaderInfo , completeCheck , errorCheck , progressCheck , openCheck , errorCheck , errorCheck);
			addLoaderListeners(loader.contentLoaderInfo , completeHandler , ioErrorHandler , progressHandler , openHandler , asyncErrorHandler , securityErrorHandler);
			
			if (!key)
			{
				key = url;
			}
			
			_loaderByKey[key] = loader;
			_urlByLoader[loader] = url;
			_keyByLoaderInfo[loader.contentLoaderInfo] = key;
			_itemsTotal++;
		}
		
		private function completeCheck (event:Event):void
		{
			event.stopPropagation();
			removeLoaderListeners(IEventDispatcher(event.target) , completeCheck , errorCheck , progressCheck , openCheck , errorCheck , errorCheck);
			var key:* = _keyByLoaderInfo[event.target];
			
			if (event.target is URLLoader)
			{
				_itemByKey[key] = event.target.data;
			}
			else if (event.target is Sound)
			{
				_itemByKey[key] = event.target;
			}
			else if (event.target is LoaderInfo)
			{
				_itemByKey[key] = event.target.content;
			}
			else
			{
				_itemByKey[key] = null;
				trace("BulletLoader: completeCheck: Received an event target for key '" + key + "' that does not correspond with a data, sound, or display load.");
			}
			
			delete _urlByLoader[_loaderByKey[key]];
			delete _loaderByKey[key];
			delete _keyByLoaderInfo[event.target];
			
			_itemsLoaded++;
			_itemRatio = (_itemsLoaded + _ioErrors) / _itemsTotal;
			
			dispatchEvent(new Event(ITEM_COMPLETE , event.bubbles , event.cancelable));
			
			if (_itemRatio == 1)
			{
				dispatchEvent(new Event(COMPLETE));
			}
		}
		
		private function progressCheck (event:ProgressEvent):void
		{
			if (!_bytesTotalIndex[event.target])
			{
				_bytesTotalIndex[event.target] = event.bytesTotal;
				_bytesTotal += event.bytesTotal;
				_bytesLoadedIndex[event.target] = 0;
			}
			else
			{
				_bytesLoaded += event.bytesLoaded - _bytesLoadedIndex[event.target];
				_bytesLoadedIndex[event.target] = event.bytesLoaded;
			}
			
			if (_opened)
			{
				_byteRatio = _bytesLoaded / _bytesTotal;
				dispatchEvent(new ProgressEvent(event.type , event.bubbles , event.cancelable , _bytesLoaded , _bytesTotal));
			}
		}
		
		private function openCheck (event:Event):void
		{
			event.target.removeEventListener(OPEN , openCheck);
			_itemsOpened++;
			
			dispatchEvent(new Event(ITEM_OPEN , event.bubbles , event.cancelable));
			checkBulletLoadOpen();
		}
		
		private function errorCheck (event:*):void
		{
			removeLoaderListeners(IEventDispatcher(event.target) , completeCheck , errorCheck , progressCheck , openCheck , errorCheck , errorCheck);
			
			_ioErrors++;
			_itemRatio = (_itemsLoaded + _ioErrors) / _itemsTotal;
			
			var key:* = _keyByLoaderInfo[event.target];
			
			if (dispatchErrors)
			{
				event.text = "BulletLoader IOError: key: '" + key + "' url: '" + _urlByLoader[_loaderByKey[key]] + "' message: '" + event.text + "'";
				dispatchEvent(event);
			}
			
			delete _urlByLoader[_loaderByKey[key]];
			delete _loaderByKey[key];
			delete _keyByLoaderInfo[event.target];
			
			checkBulletLoadOpen();
			if (_itemRatio == 1)
			{
				dispatchEvent(new Event(COMPLETE));
			}
		}
		
		//EXPERIMENTAL START
		public function addBulletLoad (quickLoad:BulletLoader , integrateLoads:Boolean = true):void
		{
			if (integrateLoads)
			{
				quickLoad.addEventListener(ITEM_OPEN , onBulletLoadItemOpen);
				quickLoad.addEventListener(OPEN , onQuickLoadOpen);
				quickLoad.addEventListener(PROGRESS , progressCheck);
				quickLoad.addEventListener(ITEM_COMPLETE , onBulletLoadItemComplete);
				quickLoad.addEventListener(ITEM_IO_ERROR , onBulletLoadItemioError);
				quickLoad.addEventListener(COMPLETE , onBulletLoadComplete);
				_itemsTotal += quickLoad.itemsTotal;
			}
			else
			{
				addLoaderListeners(quickLoad , completeCheck , null , progressCheck , openCheck);
				_itemsTotal++;
			}
		}
		
		private function onBulletLoadItemOpen (event:Event):void
		{
			_itemsOpened++;
			dispatchEvent(new Event(ITEM_OPEN , event.bubbles , event.cancelable));
			checkBulletLoadOpen();
		}
		
		private function onQuickLoadOpen (event:Event):void
		{
			event.target.removeEventListener(ITEM_OPEN , onBulletLoadItemOpen);
			event.target.removeEventListener(OPEN , onBulletLoadItemOpen);
		}
		
		private function onBulletLoadItemComplete (event:Event):void
		{
			event.stopPropagation();
			_itemsLoaded++;
			_itemRatio = (_itemsLoaded + _ioErrors) / _itemsTotal;
			dispatchEvent(new Event(ITEM_COMPLETE , event.bubbles , event.cancelable));
		}
		
		private function onBulletLoadItemioError (event:Event):void
		{
			_ioErrors++;
			_itemRatio = (_itemsLoaded + _ioErrors) / _itemsTotal;
			checkBulletLoadOpen();
		}
		
		private function onBulletLoadComplete (event:Event):void
		{
			event.target.removeEventListener(PROGRESS , progressCheck);
			event.target.removeEventListener(ITEM_COMPLETE , onBulletLoadComplete);
			event.target.removeEventListener(ITEM_IO_ERROR , onBulletLoadItemioError);
			event.target.removeEventListener(COMPLETE , onBulletLoadItemComplete);
			
			if (_itemRatio == 1)
			{
				dispatchEvent(new Event(COMPLETE));
			}
		}
		//EXPERIMENTAL END
		
		
		private function checkBulletLoadOpen ():void
		{
			if (!openAllBeforeProgress || _itemsOpened + _ioErrors == _itemsTotal)
			{
				_opened = true;
				dispatchEvent(new Event(OPEN));
			}
		}
		
		public function removeByURL(url:String):void
		{
			var loader:* = _loaderByKey[url];
			if (loader)
			{
				if (_loading)
				{
					var loaderEventDispatcher:IEventDispatcher;
					if (loader is URLLoader)
					{
						loaderEventDispatcher = loader;
					}
					else
					{
						loaderEventDispatcher = loader.contentLoaderInfo;
					}
					
					removeLoaderListeners(loaderEventDispatcher , completeCheck , errorCheck , progressCheck , openCheck);
					loader.close();
					_bytesTotal -= _bytesTotalIndex[loader];
					_bytesLoaded -= _bytesLoadedIndex[loader];
					_byteRatio = _bytesLoaded / _bytesTotal;
					_itemsTotal--;
					_itemsOpened--;
					
					if (_bytesTotalIndex[loader] == _bytesLoadedIndex[loader])
					{
						_itemsLoaded--;
					}
					
					_itemRatio = (_itemsLoaded + _ioErrors) / _itemsTotal;
				}
				
				delete _urlByLoader[loader];
				delete _keyByLoaderInfo[loader];
				delete _loaderByKey[url];
			}
		}
		
		public function removeByLoader(loaderObj:IEventDispatcher):void
		{
			var loader:* = loaderObj;
			if (_loading) {
				loader.close();
				_bytesTotal -= _bytesTotalIndex[loader];
				_bytesLoaded -= _bytesLoadedIndex[loader];
				_byteRatio = _bytesLoaded / _bytesTotal;
				_itemsTotal--;
				_itemsOpened--;
				
				if (_bytesTotalIndex[loader] == _bytesLoadedIndex[loader])
				{
					_itemsLoaded--;
				}
				
				_itemRatio = (_itemsLoaded + _ioErrors) / _itemsTotal;
			}
			
			delete _loaderByKey[_urlByLoader[loaderObj]];
			delete _urlByLoader[loaderObj];
			delete _keyByLoaderInfo[loader];
		}
		
		public function removeItem (key:*):Boolean
		{
			var itemExists:Boolean = Boolean(_itemByKey[key]);
			delete _itemByKey[key];
			return itemExists;
		}
		
		public function load():void
		{
			_loading = true;
			
			var defaultContext:LoaderContext;
			if (this.displayContext)
			{
				defaultContext = this.displayContext;
			}
			else
			{
				defaultContext = new LoaderContext(false , ApplicationDomain.currentDomain);
			}
			
			for each (var loader:* in _loaderByKey)
			{
				if (_urlByLoader[loader])
				{
					if (loader is Loader)
					{
						if (!_loadsMax || _loadsRunning < _loadsMax)
						{
							_loadsRunning++;
							loader.contentLoaderInfo.addEventListener(COMPLETE , completeCheckGlobal , false , int.MAX_VALUE);
							loader.contentLoaderInfo.addEventListener(IO_ERROR , errorCheckGlobal , false , int.MAX_VALUE);
							loader.load(new URLRequest(BulletPath.applyPath(_urlByLoader[loader])) , defaultContext);
						}
						else
						{
							_loadQueue.push(loader);
							_loadArguments[loader] = new Array(new URLRequest(BulletPath.applyPath(_urlByLoader[loader])) , defaultContext);
						}
					}
					else if (loader is URLLoader)
					{
						if (!_loadsMax || _loadsRunning < _loadsMax)
						{
							_loadsRunning++;
							loader.addEventListener(COMPLETE , completeCheckGlobal , false , int.MAX_VALUE);
							loader.addEventListener(IO_ERROR , errorCheckGlobal , false , int.MAX_VALUE);
							loader.load(new URLRequest(BulletPath.applyPath(_urlByLoader[loader])));
						}
						else
						{
							_loadQueue.push(loader);
							_loadArguments[loader] = new Array(new URLRequest(BulletPath.applyPath(_urlByLoader[loader])));
						}
					}
					else if (loader is Sound)
					{
						if (!_loadsMax || _loadsRunning < _loadsMax)
						{
							_loadsRunning++;
							loader.addEventListener(COMPLETE , completeCheckGlobal , false , int.MAX_VALUE);
							loader.addEventListener(IO_ERROR , errorCheckGlobal , false , int.MAX_VALUE);
							loader.load(new URLRequest(BulletPath.applyPath(_urlByLoader[loader])) , _soundContext);
						}
						else
						{
							_loadQueue.push(loader);
							_loadArguments[loader] = new Array(new URLRequest(BulletPath.applyPath(_urlByLoader[loader])) , _soundContext);
						}
					}
					else
					{
						throw(new Error("BulletLoader: load: Error: urlByLoader did not return a Loader or URLLoader object. The following object was found: '" + loader + "'"));
					}
				}
				else
				{
					throw(new Error("BulletLoader: load: Error: URL not found in _urlByLoader."));
				}
			}
		}
		
		public function close():void
		{
			for each (var loader:* in this._loaderByKey)
			{
				if (loader is URLLoader || loader is Sound)
				{
					removeLoaderListeners(loader , completeCheck , errorCheck , progressCheck , openCheck);
					loader.removeEventListener(COMPLETE , completeCheckGlobal);
					loader.removeEventListener(IO_ERROR , errorCheckGlobal);
				}
				else if (loader is Loader)
				{
					removeLoaderListeners(loader.contentLoaderInfo , completeCheck , errorCheck , progressCheck , openCheck);
					loader.contentLoaderInfo.removeEventListener(COMPLETE , completeCheckGlobal);
					loader.contentLoaderInfo.removeEventListener(IO_ERROR , errorCheckGlobal);
				}
				
				if (_loading)
				{
					loader.close();
				}
				
				delete _urlByLoader[loader];
			}
			_loading = false;
			reset();
		}
		
		public function reset():void
		{
			if (!loading)
			{
				_bytesTotalIndex = new Dictionary();
				_bytesLoadedIndex = new Dictionary();
				_bytesTotal = 0;
				_bytesLoaded = 0;
				_byteRatio = NaN;
				
				_itemsTotal = 0;
				_itemsOpened = 0;
				_itemsLoaded = 0;
				_itemRatio = NaN;
			}
		}
		
		public function getItem (key:* , remove:Boolean = true):*
		{
			var item:* = _itemByKey[key];
			
			if (item == null)
			{
				throw(new Error("BulletLoader: getItem: Error: No item found for the given key: '" + key + "'"));
			}
			
			if (remove)
			{
				delete _itemByKey[key];
			}
			
			return item;
		}
		
		public function hasItem (key:*):Boolean
		{
			return _itemByKey[key] != null;
		}
		
		public function hasKey (key:*):Boolean
		{
			return _loaderByKey[key] || _itemByKey[key];
		}
		
		public function get loaderByKey ():Dictionary
		{
			return _loaderByKey;
		}
		
		public function get urlByLoader ():Dictionary
		{
			return _urlByLoader;
		}
		
		public function get keyByLoaderInfo ():Dictionary
		{
			return _itemByKey;
		}
		
		public function get itemByKey ():Dictionary
		{
			return _itemByKey;
		}
		
		public function get loading ():Boolean
		{
			return _loading;
		}
		
		public function get bytesTotalIndex ():Dictionary
		{
			return _bytesTotalIndex;
		}
		
		public function get bytesLoadedIndex ():Dictionary
		{
			return _bytesLoadedIndex;
		}
		
		public function get bytesTotal ():uint
		{
			return _bytesTotal;
		}
		
		public function get bytesLoaded ():uint
		{
			return _bytesLoaded;
		}
		
		public function get byteRatio ():Number
		{
			return _byteRatio;
		}
		
		public function get itemsTotal ():uint
		{
			return _itemsTotal;
		}
		
		public function get itemsOpened ():uint
		{
			return _itemsOpened;
		}
		
		public function get itemsLoaded ():uint
		{
			return _itemsLoaded;
		}
		
		public function get ioErrors ():uint
		{
			return _ioErrors;
		}
		
		public function get itemRatio ():Number
		{
			return _itemRatio;
		}
	}
	
}